package resources

import (
	"context"
	goerr "errors"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go-v2/service/iam/types"
	"github.com/gruntwork-io/cloud-nuke/report"
	"github.com/gruntwork-io/cloud-nuke/util"
	"github.com/hashicorp/go-multierror"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/iam"
	"github.com/gruntwork-io/cloud-nuke/config"
	"github.com/gruntwork-io/cloud-nuke/logging"
	"github.com/gruntwork-io/go-commons/errors"
	"github.com/gruntwork-io/go-commons/retry"
)

// List all IAM users in the AWS account and returns a slice of the UserNames
func (iu *IAMUsers) getAll(c context.Context, configObj config.Config) ([]*string, error) {
	var userNames []*string

	input := &iam.ListUsersInput{}
	paginator := iam.NewListUsersPaginator(iu.Client, input)
	for paginator.HasMorePages() {
		page, err := paginator.NextPage(c)
		if err != nil {
			return nil, errors.WithStackTrace(err)
		}

		for _, user := range page.Users {
			// Note :
			// IAM resource-listing operations return a subset of the available attributes for the resource.
			// This operation does not return the following attributes, even though they are an attribute of the returned object:
			//    PermissionsBoundary
			//    Tags
			// Referene : https://docs.aws.amazon.com/cli/latest/reference/iam/list-users.html

			var tags []types.Tag

			tagsPaginator := iam.NewListUserTagsPaginator(iu.Client, &iam.ListUserTagsInput{
				UserName: user.UserName,
			})
			for tagsPaginator.HasMorePages() {
				tagsPage, errListTags := tagsPaginator.NextPage(c)
				if errListTags != nil {
					return nil, errors.WithStackTrace(errListTags)
				}

				tags = append(tags, tagsPage.Tags...)
			}

			if configObj.IAMUsers.ShouldInclude(config.ResourceValue{
				Name: user.UserName,
				Time: user.CreateDate,
				Tags: util.ConvertIAMTagsToMap(tags),
			}) {
				userNames = append(userNames, user.UserName)
			}
		}
	}

	return userNames, nil
}

func (iu *IAMUsers) detachUserPolicies(userName *string) error {
	policiesOutput, err := iu.Client.ListAttachedUserPolicies(iu.Context, &iam.ListAttachedUserPoliciesInput{
		UserName: userName,
	})
	if err != nil {
		return errors.WithStackTrace(err)
	}

	for _, attachedPolicy := range policiesOutput.AttachedPolicies {
		arn := attachedPolicy.PolicyArn
		_, err = iu.Client.DetachUserPolicy(iu.Context, &iam.DetachUserPolicyInput{
			PolicyArn: arn,
			UserName:  userName,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}
		logging.Debugf("Detached Policy %s from User %s", aws.ToString(arn), aws.ToString(userName))
	}

	return nil
}

func (iu *IAMUsers) deleteInlineUserPolicies(userName *string) error {
	policyOutput, err := iu.Client.ListUserPolicies(iu.Context, &iam.ListUserPoliciesInput{
		UserName: userName,
	})
	if err != nil {
		logging.Errorf("[Failed] %s", err)
		return errors.WithStackTrace(err)
	}

	for _, policyName := range policyOutput.PolicyNames {
		_, err := iu.Client.DeleteUserPolicy(iu.Context, &iam.DeleteUserPolicyInput{
			PolicyName: aws.String(policyName),
			UserName:   userName,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}
		logging.Debugf("Deleted Inline Policy %s from User %s", policyName, aws.ToString(userName))
	}

	return nil
}

func (iu *IAMUsers) removeUserFromGroups(userName *string) error {
	groupsOutput, err := iu.Client.ListGroupsForUser(iu.Context, &iam.ListGroupsForUserInput{
		UserName: userName,
	})
	if err != nil {
		return errors.WithStackTrace(err)
	}

	for _, group := range groupsOutput.Groups {
		_, err := iu.Client.RemoveUserFromGroup(iu.Context, &iam.RemoveUserFromGroupInput{
			GroupName: group.GroupName,
			UserName:  userName,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}
		logging.Debugf("Removed user %s from group %s", aws.ToString(userName), aws.ToString(group.GroupName))
	}

	return nil
}

func (iu *IAMUsers) deleteLoginProfile(userName *string) error {
	return retry.DoWithRetry(
		logging.Logger.WithTime(time.Now()),
		"Delete Login Profile",
		10,
		2*time.Second,
		func() error {
			// Delete Login Profile attached to the user
			_, err := iu.Client.DeleteLoginProfile(iu.Context, &iam.DeleteLoginProfileInput{
				UserName: userName,
			})
			if err != nil {
				var (
					errNoSuchEntityException                  *types.NoSuchEntityException
					errEntityTemporarilyUnmodifiableException *types.EntityTemporarilyUnmodifiableException
				)
				switch {
				case goerr.As(err, &errNoSuchEntityException):
					// This is expected if the user doesn't have a Login Profile
					// (automated users created via API calls withouth further
					// configuration)
					return nil
				case goerr.As(err, &errEntityTemporarilyUnmodifiableException):
					// The request was rejected because it referenced an entity that is
					// temporarily unmodifiable. We have to try again.
					return fmt.Errorf("Login Profile for user %s cannot be deleted now", aws.ToString(userName))
				default:
					return retry.FatalError{Underlying: err}
				}
			}

			logging.Debugf("Deleted Login Profile from user %s", aws.ToString(userName))
			return nil
		})
}

func (iu *IAMUsers) deleteAccessKeys(userName *string) error {
	output, err := iu.Client.ListAccessKeys(iu.Context, &iam.ListAccessKeysInput{
		UserName: userName,
	})
	if err != nil {
		logging.Debugf("[Failed] %s", err)
		return errors.WithStackTrace(err)
	}

	for _, md := range output.AccessKeyMetadata {
		accessKeyId := md.AccessKeyId
		_, err := iu.Client.DeleteAccessKey(iu.Context, &iam.DeleteAccessKeyInput{
			AccessKeyId: accessKeyId,
			UserName:    userName,
		})
		if err != nil {
			logging.Debugf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}

		logging.Debugf("Deleted Access Key %s from user %s", aws.ToString(accessKeyId), aws.ToString(userName))
	}

	return nil
}

func (iu *IAMUsers) deleteSigningCertificate(userName *string) error {
	output, err := iu.Client.ListSigningCertificates(iu.Context, &iam.ListSigningCertificatesInput{
		UserName: userName,
	})
	if err != nil {
		logging.Errorf("[Failed] %s", err)
		return errors.WithStackTrace(err)
	}

	for _, cert := range output.Certificates {
		certificateId := cert.CertificateId
		_, err := iu.Client.DeleteSigningCertificate(iu.Context, &iam.DeleteSigningCertificateInput{
			CertificateId: certificateId,
			UserName:      userName,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}

		logging.Debugf("Deleted Signing Certificate ID %s from user %s", aws.ToString(certificateId), aws.ToString(userName))
	}

	return nil
}

func (iu *IAMUsers) deleteSSHPublicKeys(userName *string) error {
	output, err := iu.Client.ListSSHPublicKeys(iu.Context, &iam.ListSSHPublicKeysInput{
		UserName: userName,
	})
	if err != nil {
		logging.Errorf("[Failed] %s", err)
		return errors.WithStackTrace(err)
	}

	for _, key := range output.SSHPublicKeys {
		keyId := key.SSHPublicKeyId
		_, err := iu.Client.DeleteSSHPublicKey(iu.Context, &iam.DeleteSSHPublicKeyInput{
			SSHPublicKeyId: keyId,
			UserName:       userName,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}

		logging.Debugf("Deleted SSH Public Key with ID %s from user %s", aws.ToString(keyId), aws.ToString(userName))
	}

	return nil
}

func (iu *IAMUsers) deleteServiceSpecificCredentials(userName *string) error {
	services := []string{
		"cassandra.amazonaws.com",
		"codecommit.amazonaws.com",
	}
	for _, service := range services {
		output, err := iu.Client.ListServiceSpecificCredentials(iu.Context, &iam.ListServiceSpecificCredentialsInput{
			ServiceName: aws.String(service),
			UserName:    userName,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}

		for _, metadata := range output.ServiceSpecificCredentials {
			serviceSpecificCredentialId := metadata.ServiceSpecificCredentialId

			_, err := iu.Client.DeleteServiceSpecificCredential(iu.Context, &iam.DeleteServiceSpecificCredentialInput{
				ServiceSpecificCredentialId: serviceSpecificCredentialId,
				UserName:                    userName,
			})
			if err != nil {
				logging.Errorf("[Failed] %s", err)
				return errors.WithStackTrace(err)
			}

			logging.Debugf("Deleted Service Specific Credential with ID %s of service %s from user %s", aws.ToString(serviceSpecificCredentialId), service, aws.ToString(userName))
		}
	}

	return nil
}

func (iu *IAMUsers) deleteMFADevices(userName *string) error {
	output, err := iu.Client.ListMFADevices(iu.Context, &iam.ListMFADevicesInput{
		UserName: userName,
	})
	if err != nil {
		logging.Errorf("[Failed] %s", err)
		return errors.WithStackTrace(err)
	}

	// First we need to deactivate the devices
	for _, device := range output.MFADevices {
		serialNumber := device.SerialNumber

		_, err := iu.Client.DeactivateMFADevice(iu.Context, &iam.DeactivateMFADeviceInput{
			SerialNumber: serialNumber,
			UserName:     userName,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}

		logging.Debugf("Deactivated Virtual MFA Device with ID %s from user %s", aws.ToString(serialNumber), aws.ToString(userName))
	}

	// After their deactivation we can delete them
	for _, device := range output.MFADevices {
		serialNumber := device.SerialNumber

		_, err := iu.Client.DeleteVirtualMFADevice(iu.Context, &iam.DeleteVirtualMFADeviceInput{
			SerialNumber: serialNumber,
		})
		if err != nil {
			logging.Errorf("[Failed] %s", err)
			return errors.WithStackTrace(err)
		}

		logging.Debugf("Deleted Virtual MFA Device with ID %s from user %s", aws.ToString(serialNumber), aws.ToString(userName))
	}

	return nil
}

func (iu *IAMUsers) deleteUser(userName *string) error {
	_, err := iu.Client.DeleteUser(iu.Context, &iam.DeleteUserInput{
		UserName: userName,
	})
	if err != nil {
		return errors.WithStackTrace(err)
	}

	return nil
}

// Nuke a single user
func (iu *IAMUsers) nukeUser(userName *string) error {
	// Functions used to really nuke an IAM User as a user can have many attached
	// items we need delete/detach them before actually deleting it.
	// NOTE: The actual user deletion should always be the last one. This way we
	// can guarantee that it will fail if we forgot to delete/detach an item.
	functions := []func(userName *string) error{
		iu.detachUserPolicies, // TODO: Add CLI option to delete the Policy as policies exist independently of the user
		iu.deleteInlineUserPolicies,
		iu.removeUserFromGroups, // TODO: Add CLI option to delete groups as groups exist independently of the user
		iu.deleteLoginProfile,
		iu.deleteAccessKeys,
		iu.deleteSigningCertificate,
		iu.deleteSSHPublicKeys,
		iu.deleteServiceSpecificCredentials,
		iu.deleteMFADevices,
		iu.deleteUser,
	}

	for _, fn := range functions {
		if err := fn(userName); err != nil {
			return err
		}
	}

	return nil
}

// Delete all IAM Users
func (iu *IAMUsers) nukeAll(userNames []*string) error {
	if len(userNames) == 0 {
		logging.Info("No IAM Users to nuke")
		return nil
	}

	logging.Info("Deleting all IAM Users")

	deletedUsers := 0
	multiErr := new(multierror.Error)

	for _, userName := range userNames {
		err := iu.nukeUser(userName)
		// Record status of this resource
		e := report.Entry{
			Identifier:   aws.ToString(userName),
			ResourceType: "IAM User",
			Error:        err,
		}
		report.Record(e)

		if err != nil {
			logging.Errorf("[Failed] %s", err)
			multierror.Append(multiErr, err)
		} else {
			deletedUsers++
			logging.Debugf("Deleted IAM User: %s", *userName)
		}
	}

	logging.Debugf("[OK] %d IAM User(s) terminated", deletedUsers)
	return multiErr.ErrorOrNil()
}
